package org.upida.validation.impl;

import java.util.Collection;
import java.util.Deque;

import org.upida.Dtobase;
import org.upida.IUpidaContext;
import org.upida.validation.Failure;
import org.upida.validation.IChecker;
import org.upida.validation.IFailureList;
import org.upida.validation.IMath;
import org.upida.validation.IPathHelper;
import org.upida.validation.IUpidaValidationContext;
import org.upida.validation.ValidationException;

@SuppressWarnings("rawtypes")
public class UpidaValidationContext implements IUpidaValidationContext {

	private final IMath math;
	private final IPathHelper pathHelper;
	private final IChecker checker;
	private final Deque<PathNode> path;

	protected String fieldName;
	protected Object fieldValue;
	protected IFailureList failures;

	private boolean isValid;
	private boolean isTargetValid;
	private boolean isFieldValid;

	/**
	 * initializes new instance of the ValidationContext class
	 */
	public UpidaValidationContext(IUpidaContext context) {
		this.math = context.getMath();
		this.pathHelper = context.getPathHelper();
		this.checker = context.getChecker();
		this.path = this.pathHelper.createNew();
	}

	/**
	 * false, if at least on failure is recorded, otherwise true
	 */
	@Override
	public boolean isValid() {
		return this.isValid;
	}

	/**
	 * false, if at least on failure is recorded for the current target, otherwise true
	 */
	@Override
	public boolean isTargetValid() {
		return this.isTargetValid;
	}

	/**
	 * false, if at least on failure is recorded for the current field, otherwise true
	 */
	@Override
	public boolean isFieldValid() {
		return this.isFieldValid;
	}

	/**
	 * sets current target object
	 */
	@Override
	public void setTarget(Dtobase value) {
		this.pathHelper.setTopNodeTarget(this.path, value);
		this.isTargetValid = true;
	}

	/**
	 * Sets current index (used for indexed properties).
	 * For ex. current target is 'Children' and index is '7' and field 'Name' - failure would have this path - 'children[7].name'.
	 */
	@Override
	public void setIndex(int index) {
		this.path.getLast().setIndex(index);
	}

	/**
	 * sets current field (property) name
	 */
	@Override
	public void setField(String name, Object value) {
		this.fieldValue = value;
		this.fieldName = name;
		this.isFieldValid = true;
	}

	/**
	 * Propagates current field to path.
	 * For ex. if current field is 'User', call AddNested(), then call SetField("Name", null) - the failure path would be - User.Name.
	 * Usually after AddNested() call, SetTarget() must be called as well.
	 */
	@Override
	public void addNested() {
		this.pathHelper.addNested(this.path, this.fieldName);
	}

	/**
	 * goes one node back in path.
	 */
	@Override
	public void removeNested() {
		this.pathHelper.removeNested(this.path);
	}

	/**
	 * registers failure without path
	 */
	@Override
	public void failRoot(String msg) {
		this.fail(new Failure(null, msg));
	}

	/**
	 * registers failure using current path
	 */
	@Override
	public void fail(String msg) {
		Failure failure = new Failure(
			this.pathHelper.buildPath(this.path, this.fieldName),
			msg);
		this.fail(failure);
	}

	/**
	 * registers failure
	 */
	@Override
	public void fail(Failure failure) {
		this.isFieldValid = false;
		this.isTargetValid = false;
		this.isValid = false;
		if (null == this.failures)
		{
			this.failures = new FailureList();
		}
		
		this.failures.fail(failure);
	}

	/**
	 * true if current field is assigned, otherwise false
	 */
	@Override
	public boolean isAssigned() {
		Dtobase target = this.pathHelper.getTopNodeTarget(this.path);
		return this.checker.isAssigned(this.fieldName, target);
	}

	/**
	 * true is current field is null, otherwise false
	 */
	@Override
	public boolean isNull() {
		return this.checker.isNull(this.fieldValue);
	}

	/**
	 * true if current field is correctly parsed, otherwise false
	 */
	@Override
	public boolean isValidFormat() {
		Dtobase target = this.pathHelper.getTopNodeTarget(this.path);
		return this.checker.isValidFormat(this.fieldName, target);
	}

	/**
	 * validates if the number of assigned fields in the current target object is less or equal to max number
	 * @param msg failure message
	 */
	@Override
	public void mustHaveMinAssignedFieldsCount(int count, String msg) {
		Dtobase target = this.pathHelper.getTopNodeTarget(this.path);
		if (null != target.readAssignedFields() &&
			count < target.readAssignedFields().size())
		{
			this.failRoot(msg);
		}
	}

	/**
	 * validates if the number of assigned fields in the current target object is greater or equal to min number
	 * @param msg failure message
	 */
	@Override
	public void mustHaveMaxAssignedFieldsCount(int count, String msg) {
		Dtobase target = this.pathHelper.getTopNodeTarget(this.path);
		if (null != target.readAssignedFields() &&
			count > target.readAssignedFields().size())
		{
			this.failRoot(msg);
		}
	}

	/**
	 * validates if current field is assigned (was present in incoming JSON)
	 * @param msg failure message
	 */
	@Override
	public void mustBeAssigned(String msg) {
		Dtobase target = this.pathHelper.getTopNodeTarget(this.path);
		if (!this.checker.isAssigned(this.fieldName, target))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value is not assigned by JSON deserializer
	 * @param msg failure message
	 */
	@Override
	public void mustBeNotAssigned(String msg) {
		Dtobase target = this.pathHelper.getTopNodeTarget(this.path);
		if (this.checker.isAssigned(this.fieldName, target))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value is correctly parsed
	 * @param msg failure message
	 */
	@Override
	public void mustBeValidFormat(String msg) {
		Dtobase target = this.pathHelper.getTopNodeTarget(this.path);
		if (!this.checker.isValidFormat(this.fieldName, target))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value is null
	 * @param msg failure message
	 */
	@Override
	public void mustBeNull(String msg) {
		if (!this.checker.isNull(this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value is not null
	 * @param msg failure message
	 */
	@Override
	public void mustBeNotNull(String msg) {
		if (this.checker.isNull(this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value is equal to the value
	 * @param msg failure message
	 */
	@Override
	public void mustEqualTo(Object value, String msg) {
		if (!this.checker.isEqualTo(value, this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value is not equal to the value
	 * @param msg failure message
	 */
	@Override
	public void mustNotEqualTo(Object value, String msg) {
		if (this.checker.isEqualTo(value, this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value is equal to one of the values
	 * @param msg failure message
	 */
	@Override
	public void mustEqualToOneOf(Object[] values, String msg) {
		if (!this.checker.isEqualToOneOf(values, this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value is not equal to any of the values
	 * @param msg failure message
	 */
	@Override
	public void mustNotEqualToAnyOf(Object[] values, String msg) {
		if (this.checker.isEqualToOneOf(values, this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value is not empty string
	 * @param msg failure message
	 */
	@Override
	public void mustBeNotEmptyString(String msg) {
		if (null == this.fieldValue) return;
		if (this.checker.isEmptyString((String)this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value length (String.length) is between min and max
	 * @param msg failure message
	 */
	@Override
	public void mustHaveLengthBetween(int min, int max, String msg) {
		if (null == this.fieldValue) return;
		if (!this.checker.isLengthBetween(min, max, (String)this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value (Comparable) is less or equal to m
	 * @param msg failure message
	 */
	@Override
	public void mustBeLessOrEqualTo(Object m, String msg) {
		if (null == this.fieldValue) return;
		if (!this.checker.isLessOrEqualTo(m, (Comparable)this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value (Comparable) is less than m
	 * @param msg failure message
	 */
	@Override
	public void mustBeLessThan(Object m, String msg) {
		if (null == this.fieldValue) return;
		if (this.checker.isLessThan(m, (Comparable)this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value (Comparable) is greater or equal to m
	 * @param msg failure message
	 */
	@Override
	public void mustBeGreaterOrEqualTo(Object m, String msg) {
		if (null == this.fieldValue) return;
		if (!this.checker.isGreaterOrEqualTo(m, (Comparable)this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value (Comparable) is greater than m
	 * @param msg failure message
	 */
	@Override
	public void mustBeGreaterThan(Object m, String msg) {
		if (null == this.fieldValue) return;
		if (!this.checker.isGreaterThan(m, (Comparable)this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value count (Collection.size()) is between min and max count
	 * @param msg failure message
	 */
	@Override
	public void mustHaveCountBetween(int min, int max, String msg) {
		if (null == this.fieldValue) return;
		if (!this.checker.isCountBetween(min, max, (Collection<?>)this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * validates if current field value follows the regular expression
	 * @param msg failure message
	 */
	@Override
	public void mustRegexpr(String expr, String msg) {
		if (null == this.fieldValue) return;
		if (!this.checker.isRegexpr(expr, (String)this.fieldValue))
		{
			this.fail(msg);
		}
	}

	/**
	 * throws ValidationException if at least one failure was registered
	 */
	@Override
	public void assertValid() {
		if (null != this.failures)
		{
			throw new ValidationException(this.failures);
		}
	}

	/**
	 * gets Math helper object
	 */
	@Override
	public IMath getMath() {
		return this.math;
	}
}